﻿using System;
using System.IO.Ports;
using System.Collections;
using Microsoft.SPOT;
using MFGps.Data;

namespace MFGps.Devices
{
    /// <summary>
    /// Class manages communication betweeen a NEMA capabile GPS device (such as the Sirf 3)
    /// </summary>
    public abstract class NmeaGps : IGpsDevice
    {
		string message;

        /// <summary>
        /// The serial port that the GPS device is on
        /// </summary>
        private SerialPort serialPort;

        /// <summary>
        /// Fired when the location changes
        /// </summary>
        public event PositionChangedDelegate PositionChanged;

        /// <summary>
        /// Event is raised when the GPS position has encounered a waypoint
        /// </summary>
        public event WaypointEncounteredDelegate WaypointEncountered;


        #region Constructors
        /// <summary>
        /// Connects to the default GPS device on the UART (com2) port
        /// </summary>
        public NmeaGps()
        {
            BaudRate = 9600;
        }

        /// <summary>
        /// Connects to the specified com port
        /// </summary>
        /// <param name="ComPort">Specify the com port that should be used.</param>
        public NmeaGps(string comPort, int baudRate)
        {
			ComPort = comPort;
			BaudRate = baudRate;
        }

        /// <summary>
        /// Connects to the specified com port and if to enable the update event
        /// </summary>
        /// <param name="ComPort">Specify the com port that should be used.</param>
        /// <param name="EnableEventUpdate">If true, instructs the device to raise the PositionChanged event when an updated location becomes avaliable</param>
        public NmeaGps(string comPort, int baudRate, bool enableEventUpdate)
        {
			ComPort = comPort;
			BaudRate = baudRate;
            PositionUpdateEnabled = enableEventUpdate;
        }

        /// <summary>
        /// Connects to the specified com port and if to enable the update event, while specifying the local time to allow for faster location lock on
        /// </summary>
        /// <param name="ComPort">Specify the com port that should be used.</param>
        /// <param name="EnableEventUpdate">If true, instructs the device to raise the PositionChanged event when an updated location becomes avaliable</param>
        /// <param name="initilizeTime">DateTime to initilize the GPS device with. This allows for faster Position lock</param>
        public NmeaGps(string comPort, int baudRate, bool enableEventUpdate, DateTime initilizeTime)
        {
            throw new NotImplementedException();
        }
        #endregion

        #region Connection
        public bool Connect()
        {
            if (BaudRate == 0 || ComPort == null)
                return false;

            serialPort = new SerialPort(ComPort, BaudRate, Parity.None, 8, StopBits.One) { ReadTimeout = 0 };
            serialPort.Flush();
            serialPort.Open();

            bool success = false;

            if (serialPort.IsOpen)
            {
                success = true;

                serialPort.DataReceived += SerialPortDataReceived;
                serialPort.ErrorReceived += serialPort_ErrorReceived;
            }

            return success;
        }
        
        public bool Connect(string comPort)
        {
            ComPort = comPort;
            return Connect();
        }

        public bool Connect(string comPort, int baudRate)
        {
            ComPort = comPort;
            BaudRate = baudRate;
            return Connect();
        }

        public bool Connect(string comPort, int baudRate, DateTime localTime)
        {
            ComPort = comPort;
            BaudRate = baudRate;
            LocalTime = localTime;

            return Connect();
        }

        public bool Connect(string comPort, int baudRate, bool raisePositionEvent)
        {
            ComPort = comPort;
            BaudRate = baudRate;
            PositionUpdateEnabled = raisePositionEvent;

            return Connect();
        }

        public bool Connect(string comPort, int baudRate, bool raisePositionEvent, DateTime localTime)
        {
            ComPort = comPort;
            BaudRate = baudRate;
            PositionUpdateEnabled = raisePositionEvent;
            LocalTime = localTime;

            return Connect();
        }

        public bool Connect(string comPort, int baudRate, bool raisePositionEvent, bool raiseWaypointEvent)
        {
            ComPort = comPort;
            BaudRate = baudRate;
            PositionUpdateEnabled = raisePositionEvent;
            WaypointNotificationEnabled = raiseWaypointEvent;

            return Connect();
        }

        public bool Connect(string comPort, int baudRate, bool raisePositionEvent, bool raiseWaypointEvent, DateTime localTime)
        {
            ComPort = comPort;
            BaudRate = baudRate;
            PositionUpdateEnabled = raisePositionEvent;
            WaypointNotificationEnabled = raiseWaypointEvent;
            LocalTime = localTime;

            return Connect();
        }
        #endregion

        public void Disconnect()
        {
            // Delete the event handler since the serial conncetion is closed, what about removing the waypoints?
            // Or let the dispose method handle that?
            serialPort.DataReceived -= SerialPortDataReceived;
            serialPort.Close();
        }

        void serialPort_ErrorReceived(object sender, SerialErrorReceivedEventArgs e)
        {
            // for debugging purposes
			Debug.Print("NMEA error");
        }

        void SerialPortDataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            int bytestoread = serialPort.BytesToRead;
            byte[] response = new byte[bytestoread];
            string fractured_message = String.Empty;
            string[] message_queue = new string[100];
            string message_check;

            serialPort.Read(response, 0, bytestoread);

            fractured_message = new string(System.Text.UTF8Encoding.UTF8.GetChars(response));

            message += fractured_message;

            // truncate fractured section of message at the beginning
            int startIndex = message.IndexOf("$");
            int newLine = message.IndexOf("\r\n");
            if (newLine < startIndex)
            {
                message = message.Substring(startIndex, message.Length - message.Substring(0, startIndex).Length);

                startIndex = message.IndexOf("$");
                newLine = message.IndexOf("\r\n");
            }

            int k = -1;
            int sentence_length = 0;

            while (startIndex > -1 && newLine > -1)
            {
                message_check = message.Substring(startIndex, newLine).Trim();

                if (IsValidChecksum(message_check))
                {
                    message_queue[++k] = message_check;
                }

                // remove the message regardless...
                sentence_length = message.Substring(startIndex, newLine).Length;
                message = message.Substring(sentence_length, message.Length - sentence_length);

                startIndex = message.IndexOf("$");
                newLine = (message.Length > 3) ? message.IndexOf("\r\n", 3) : -1;
            }

            // iterate through the queue and parse the message
            for (int i = 0; i < 100; i++)
            {
                if (message_queue[i] != null && message_queue[i] != "")
                {
                    string[] split_messages = message_queue[i].Split(',');

                    switch (split_messages[0])
                    {
                        case "$GPRMC":
                            RMCDataReceived(split_messages[1], split_messages[2], split_messages[3], split_messages[4], split_messages[5], split_messages[6], split_messages[7]);
                            break;
                        case "$GPGLL":
                            GLLDataReceived(split_messages[1], split_messages[2], split_messages[3], split_messages[4], split_messages[5], split_messages[6]);
                            break;
                        default:
                            break;
                    }

                    message_queue[i] = null;
                }
                else
                {
                    break;
                }
            }


            if (PositionUpdateEnabled && PositionChanged != null)
            {
                PositionChanged(Latitude, Longitude, Altitude, LocalTime);
            }
        }

        private void GLLDataReceived(string latitude, string ns, string longitude, string ew, string time, string status)
        {
            switch (status)
            {
                case "A":
                    SatelliteFixStatus = FixStatus.Valid;
                    break;
                case "V":
                    SatelliteFixStatus = FixStatus.Invalid;
                    break;
                default:
                    SatelliteFixStatus = FixStatus.Invalid;
                    break;
            }

            Latitude = DegreeTime.FromNmeaPos(latitude, ns);
            Longitude = DegreeTime.FromNmeaPos(longitude, ew);

            //todo: process time
        }

        private void RMCDataReceived(string time, string status, string latitude, string ns, string longitude, string ew, string date)
        {
            switch (status)
            {
                case "A":
                    SatelliteFixStatus = FixStatus.Valid;
                    break;
                case "V":
                    SatelliteFixStatus = FixStatus.Invalid;
                    break;
                default:
                    SatelliteFixStatus = FixStatus.Invalid;
                    break;
            }


            Latitude = DegreeTime.FromNmeaPos(latitude, ns);
            Longitude = DegreeTime.FromNmeaPos(longitude, ew);

            //todo: process date
        }

        // Specific to Proprietary messages
        public abstract void ChangeBaudRate(int newBaud);
        public abstract void ChangeUpdateRate(int newRate);

        /// <summary>
        /// Adds a waypoint to the list of monitored waypoints
        /// </summary>
        /// <param name="waypoint">The waypoint to add</param>
        public void AddWaypoint(Waypoint waypoint)
        {
            throw new System.NotImplementedException();
        }

        /// <summary>
        /// Removes a waypoint from the list of monitored waypoints
        /// </summary>
        /// <param name="waypoint">The waypoint to remove</param>
        public void RemoveWaypoint(Waypoint waypoint)
        {
            throw new NotImplementedException();
        }

        /// <param name="waypoint">The name of the waypoint remove</param>
        public void RemoveWaypoint(string waypoint)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Validates NMEA sentance to see if the checksum is correct
        /// </summary>
        /// <param name="message">Full NMEA sentance</param>
        /// <returns>bool</returns>
        public static bool IsValidChecksum(string sentence)
        {
            if (sentence.IndexOf('*') > -1)
            {
                string valid_checksum = sentence.Substring(sentence.IndexOf('*') + 1, 2);
                sentence = sentence.Substring(1, sentence.IndexOf('*') - 1);

                int checksum = 0;

                for (int i = 0; i < sentence.Length; i++)
                {
                    checksum ^= (int)sentence[i];
                }

                return (checksum == Convert.ToInt32(valid_checksum, 16));
            }
            else
                return true;
        }

        /// <summary>
        /// Converts Byte to a Hexadecimal string, like .ToString("X2") would do;
        /// </summary>
        /// <param name="b">Byte to represent has a hexadecimal string.</param>
        /// <returns></returns>
        public static string ByteToHex(byte b)
        {
            const string hex = "0123456789ABCDEF";
            int lowNibble = b & 0x0F;
            int highNibble = (b & 0xF0) >> 4;

            return new string(new char[] { hex[highNibble], hex[lowNibble] });
        }

        #region Properties
        /// <summary>
        /// Specifies the port on which the device is connected
        /// </summary>
        public string ComPort { get; protected set; }

        /// <summary>
        /// Specifies the baud rate of the device connection
        /// </summary>
        public int BaudRate { get; protected set; }

        /// <summary>
        /// Enables or disables the PositionUpdate event
        /// </summary>
        public bool PositionUpdateEnabled { get; set; }

        /// <summary>
        /// Enables or disables the WaypointEncountered event
        /// </summary>
        public bool WaypointNotificationEnabled { get; set; }

        /// <summary>
        /// The last known good calculated Altitude as reported by the device
        /// </summary>
        public float Altitude { get; private set; }

        /// <summary>
        /// The last known good heading as reported by the device
        /// </summary>
        public float Heading { get; private set; }

		/// <summary>
		/// Speed in Kmh
		/// </summary>
        public float SpeedKmh { get; private set; }

		/// <summary>
		/// Speed in meter per second
		/// </summary>
        public float SpeedMs { get; private set; }

		/// <summary>
		/// Speed in MPH
		/// </summary>
        public float SpeedMph { get; private set; }

		/// <summary>
		/// Speed in knots
		/// </summary>
        public float SpeedKts { get; private set; }

        /// <summary>
        /// The last known good Latitude as reported by the device
        /// </summary>
        public DegreeTime Latitude { get; private set; }

        /// <summary>
        /// The last know good Longitude as reported by the device
        /// </summary>
        public DegreeTime Longitude { get; private set; }

        /// <summary>
        /// The current UTC time as reported by the device
        /// </summary>
        public DateTime UtcTime { get; private set; }

        /// <summary>
        /// The current GPS date time. This time is kept current by the real time clock and is resyncronized each time updated NEMA data is received
        /// </summary>
        public DateTime LocalTime { get; protected set; }

        /// <summary>
        /// Reports on the number of Satellites used to compute the current position
        /// </summary>
        public byte SatellitesInView { get; private set; }

        /// <summary>
        /// The current status of the satelight fix
        /// </summary>
        public FixStatus SatelliteFixStatus { get; private set; }

        ArrayList satellites;

        public ArrayList Satellites { get { return satellites; } }

        RangeError rangeErrorStatistics;

        public RangeError RangeErrorStatistics { get { return rangeErrorStatistics; } }

        bool collectRangeErrorStatistics;

        public bool CollectRangeErrorStatistics
        {
            get { return collectRangeErrorStatistics; }
            set
            {
                if (collectRangeErrorStatistics == value)
                    return;

                collectRangeErrorStatistics = value;

                CollectRangeErrorStatisticsChanged();
            }
        }

        // abstract because changing what messages is sent is a proprietary message.
        internal abstract void CollectRangeErrorStatisticsChanged();

        bool allowDeadReckoning;
        /// <summary>
        /// If enabled, allows the device to estimate the location based last known position an velocity
        /// </summary>
        public bool AllowDeadReckoning
        {
            get { return allowDeadReckoning; }
            set
            {
                if (allowDeadReckoning == value)
                    return;

                allowDeadReckoning = value;

                DeadReckoningValueChanged();
            }
        }

        internal abstract void DeadReckoningValueChanged();

        /// <summary>
        /// List of waypoints that require tracking
        /// </summary>
        public ArrayList Waypoints { get; set; }

        #endregion


        #region IDisposable Members

        public void Dispose()
        {
            Disconnect();
            serialPort.Dispose();
        }

        #endregion

    }
}
